/******************************************************************************
*  Copyright(c) 2021-2023 Sietium
*
*  @file:    wave.cpp
*  @brif:    water wave
*
*  @date:    05/10/2021
*  @Author:  bird.du
*
******************************************************************************/
#include <string>
#include <cstdlib>
#include <vector>
#include <array>
#include <memory>
#include "stb_image.h"
#include "vmath.h"
#include "glad/gl-headers.h"

#include "glslprogram.h"
#include "texture.h"
#include "objectfactory.h"
#include "node.h"

struct TextureWrapper {
    GLuint buf = 0;
    GLuint vao = 0;
    GLuint texture = 0;
    unsigned char *image = nullptr;

    bool init(GLuint sampler, const char *fileName, const GLfloat *data, int size)
    {
        int width, height, bpp;
        image = stbi_load(fileName, &width, &height, &bpp, STBI_default);

        if (image == 0 || width == 0 || height == 0 || bpp < 3 || bpp > 4) {
            return false;
        }

        GLenum format = bpp == 3 ? GL_RGB : GL_RGBA;

        GLint minFilter = GL_LINEAR;
        GLint magFilter = GL_LINEAR;

        glActiveTexture(GL_TEXTURE0);
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glBindSampler(0, sampler);

        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, image);

        if ((minFilter != GL_NEAREST && minFilter != GL_LINEAR) ||
                (magFilter != GL_NEAREST && magFilter != GL_LINEAR)) {
            glGenerateMipmap(GL_TEXTURE_2D);
        }

        glGenBuffers(1, &buf);
        glBindBuffer(GL_ARRAY_BUFFER, buf);
        glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);

        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
        glEnableVertexAttribArray(0);

        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (GLvoid *)(16 * sizeof(float)));
        glEnableVertexAttribArray(1);
        return true;
    }

    void draw()
    {
        glBindTexture(GL_TEXTURE_2D, texture);
        glBindVertexArray(vao);
        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    }

    void release()
    {
        glDeleteTextures(1, &texture);
        glDeleteBuffers(1, &buf);
        glDeleteVertexArrays(1, &vao);
        if (image)
            stbi_image_free(image);
    }
};

class Textures : public Node
{
public:
    Textures()
    {
        nodeName = "Textures";
    }
protected:
    void cleanup()
    {
        currentAnisotropicTime = 0.0;
        useAnisotropic = false;
        if (glIsSampler(sampler))
            glDeleteSamplers(1, &sampler);

        cameraPosition = 90.0f;
    }

    bool startup() override
    {
        if (PerfWindow::glVersion < 330) {
            printf("Failed! The GL version is less than 3.3 !\n");
            return false;
        }

        supportedAnisotropic = isExtensionSupported("GL_EXT_texture_filter_anisotropic");
        if (supportedAnisotropic)
            glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAnisotropic);
        else {
            Log::error("%s\n", "GL_EXT_texture_filter_anisotropic isn't supported!");
            return false;
        }

        bool ok = program.build("../media/shaders/texture/texture.vert",
                                "../media/shaders/texture/texture.frag");
        if (!ok)
            return false;

        cleanup();

        glGenSamplers(1, &sampler);
        glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT);

        //top
        const GLfloat vertices3[] = {
            -1, 1, 20, 1.0,
                1, 1, 20, 1.0,
                1, 1, -20, 1.0,
                -1, 1, -20, 1.0,
                0, 0,
                0, 1,
                20, 1,
                20, 0
            };

        textWrapper[0].init(sampler, "../media/textures/texture/ceiling.png", vertices3, sizeof(vertices3));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        //left
        const GLfloat vertices1[] = {
            -1, -1, 20, 1.0,
                -1, -1, -20, 1.0,
                -1, 1, -20, 1.0,
                -1, 1, 20, 1.0,
                0, 0,
                20, 0,
                20, 1,
                0, 1
            };

        textWrapper[1].init(sampler, "../media/textures/texture/brick.png", vertices1, sizeof(vertices1));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        //right
        const GLfloat vertices2[] = {
            1, -1, 20, 1.0,
            1, -1, -20, 1.0,
            1, 1, -20, 1.0,
            1, 1, 20, 1.0,
            0, 0,
            20, 0,
            20, 1,
            0, 1
        };

        textWrapper[2].init(sampler, "../media/textures/texture/brick.png", vertices2, sizeof(vertices2));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

        //bottom
        const GLfloat vertices4[] = {
            -1, -1, 20, 1.0,
                1, -1, 20, 1.0,
                1, -1, -20, 1.0,
                -1, -1, -20, 1.0,
                0, 0,
                0, 1,
                20, 1,
                20, 0
            };

        textWrapper[3].init(sampler, "../media/textures/texture/floor.png", vertices4, sizeof(vertices4));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        glClearColor(0.0, 0.0, 0.0, 0.0);

        return true;
    }

    void render(double currentTime, double difTime)override
    {
        (void)currentTime;

        glClear(GL_COLOR_BUFFER_BIT);

        program.use();

        auto matrix = createGLMatrix((float)difTime);
        program.setUniformMatrix4fv("mvp", matrix);

        currentAnisotropicTime += difTime;
        if (currentAnisotropicTime > 1.5) {
            useAnisotropic = !useAnisotropic;
            applyAnisotropy(useAnisotropic);
            currentAnisotropicTime = 0.0;
        }

        textWrapper[0].draw();
        textWrapper[1].draw();

        textWrapper[2].draw();
        textWrapper[3].draw();
    }

    void applyAnisotropy(bool flag)
    {
        if (supportedAnisotropic) {
            float value = flag ? 1.0 : maxAnisotropic;
            for (int i = 0; i < 4; i++) {
                glBindTexture(GL_TEXTURE_2D, textWrapper[i].texture);
                glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, value);
            }
        }
    }

    vmath::mat4 createGLMatrix(float time)
    {
        vmath::mat4 modelMatrix = vmath::scale(14.0f);

        cameraPosition = cameraPosition - time * 5.0f;
        if (cameraPosition < -80.0f)
            cameraPosition = 90.0f;

        vmath::vec3 viewPosition = vmath::vec3(0, 0, cameraPosition);
        vmath::mat4 viewMatrix = vmath::lookat(viewPosition,
                                               vmath::vec3(0.0f, 0.0f, 0.0f),
                                               vmath::vec3(0.0f, 1.0f, 0.0f));
        vmath::mat4 zoomMatrix = vmath::scale(6.00f, 6.00f, 6.00f);
        vmath::mat4 mvMatrix = viewMatrix * modelMatrix * zoomMatrix;
        vmath::mat4 projMatrix = vmath::perspective(50.0f, getWindowRatio(), 0.1f, 1000.0f);
        return projMatrix * mvMatrix;
    }

    void shutdown()override
    {
        for (int i = 0; i < 4; i++)
            textWrapper[i].release();
        cleanup();
        program.clear();
    }

    bool supportedAnisotropic;
    float maxAnisotropic;
    float currentAnisotropicTime;
    bool useAnisotropic;

    GLSLProgram program;
    GLuint sampler = 0;

    struct uniforms_block {
        vmath::mat4     mvMatrix;
        vmath::mat4     viewMatrix;
        vmath::mat4     projMatrix;
    };
    float cameraPosition;
    TextureWrapper textWrapper[4];
};

REGISTER_OBJECT(Node, Textures)
